package org.example.mallchat.websocket.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.example.mallchat.common.event.UserOnlineEvent;
import org.example.mallchat.dao.UserDao;
import org.example.mallchat.domain.entity.User;
import org.example.mallchat.service.LoginService;
import org.example.mallchat.service.RoleService;
import org.example.mallchat.websocket.adapter.WebSocketAdapter;
import org.example.mallchat.websocket.domain.dto.WsChannelExtraDTO;
import org.example.mallchat.websocket.domain.vo.response.WSBaseResp;
import org.example.mallchat.websocket.domain.vo.response.WSLoginSuccess;
import org.example.mallchat.websocket.domain.vo.response.WSLoginUrl;
import org.example.mallchat.websocket.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: zzh
 * @date: 2024/12/18 14:45:14
 * @version: 1.0
 */
@Service
public class WebSocketServiceImpl  implements WebSocketService {

    /**
     * 开发包自带的微信服务接口，封装好了所有微信相关的请求操作
     * 微信公众号开发包：weixin-java-mp
     */
    @Resource
    private WxMpService wxMpService;

    @Resource
    private UserDao userDao;

    @Resource
    private LoginService loginService;

    @Resource
    private RoleService roleService;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Resource
    @Qualifier("websocketExecutor")
    private ThreadPoolTaskExecutor wsExecutor;


    //ConcurrentHashMap 实现高并发 ！线程安全 ！
    /**
     * 存放所有在线用户的 Channel 和相关信息的 Map
     * 登录信息 - Channel
     */
    private static final ConcurrentHashMap<Channel, WsChannelExtraDTO> ONLINE_WS_USER_MAP = new ConcurrentHashMap<>();

    /**
     * 临时保存登录 code - Channel 映射Map
     * maximumSize = 10000 个存储空间
     * 带有过期时间的缓存 防止内存泄漏
     */
    public static final Duration CODE_EXPIRE_TIME = Duration.ofHours(1);
    private static final Cache<Integer, Channel> WAIT_CODE_CHANNEL_MAP =  Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(CODE_EXPIRE_TIME)
            .build();

    /**
     * openId - code 映射Map
     * 方便点击授权登录的时候，我们可以openId拿到code->channel的映射关系
     * @param channel
     */
    private static final ConcurrentHashMap<String, Integer> OPEN_ID_CODE_MAP = new ConcurrentHashMap<>();
    @Override
    public void saveOpenIdCode(String openId, Integer code) {
        OPEN_ID_CODE_MAP.put(openId, code);
    }
    @Override
    public Integer getCodeByOpenId(String openId) {
        return OPEN_ID_CODE_MAP.get(openId);
    }

    @Override
    public void scanLoginSuccess(Integer codeByOpenId, Long uid) {
        // 获取channel并且移除code
        Channel channel = WAIT_CODE_CHANNEL_MAP.getIfPresent(codeByOpenId);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        // 登录成功 保存用户信息
        WsChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_USER_MAP.get(channel);
        if (ObjectUtil.isNull(wsChannelExtraDTO)){
            wsChannelExtraDTO = new WsChannelExtraDTO();
        }
        wsChannelExtraDTO.setUid(uid);
        ONLINE_WS_USER_MAP.put(channel, wsChannelExtraDTO);
        // 移除code,防止重复登录 主动移除
        WAIT_CODE_CHANNEL_MAP.invalidate(codeByOpenId);
        // 获取token
        // LoginUserService 生成token 并且保存到redis中！
        String token = loginService.login(uid);

        User user = userDao.getById(uid);
        // 查询用户角色信息
        Long power = roleService.findPowerByUid(uid);
        // 发送登录成功消息
        WSBaseResp<WSLoginSuccess> wsBaseResp = WebSocketAdapter.buildLoginSuccessResp(user,token,power);
        sendMsg(channel, wsBaseResp);

        //用户成功上线后推送事件，同时记录用户IP！
        String ip = (String) channel.attr(AttributeKey.valueOf("ip")).get();
        user.setLastOptTime(new Date());// 更新用户最后操作时间
        user.refreshIp(ip);// 更新用户最后操作IP
        // 发送事件入库
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
    }

    @Override
    public void sendMsgWaitUserClickAuth(Integer loginCode) {
        // 先获取channel
        Channel channel = WAIT_CODE_CHANNEL_MAP.getIfPresent(loginCode);
        if (ObjectUtil.isNull(channel)){
            return;
        }
        // 发送消息 构建消息体
        WSBaseResp<WSLoginUrl> wsBaseResp = WebSocketAdapter.buildScanSuccessWait();
        sendMsg(channel, wsBaseResp);
    }

    /**
     * token认证登录
     * @param channel
     * @param token
     */
    @Override
    public void authorize(Channel channel, String token) {
        //判断是否有效token
        Long validUid = loginService.getValidUid(token);
        if (ObjectUtil.isNotNull(validUid)){
            //token有效 登录成功
            WsChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_USER_MAP.get(channel);
            wsChannelExtraDTO.setUid(validUid);
            ONLINE_WS_USER_MAP.put(channel, wsChannelExtraDTO);
            // 发送登录成功消息
            User user = userDao.getById(validUid);
            // 查询用户角色信息
            Long power = roleService.findPowerByUid(validUid);
            WSBaseResp<WSLoginSuccess> wsBaseResp = WebSocketAdapter.buildLoginSuccessResp(user,token,power);
            sendMsg(channel, wsBaseResp);
        }else {
            //前端token失效
            sendMsg(channel, WebSocketAdapter.buildInvalidTokenResp());
        }
    }

    // 线程池执行发送消息
    @Override
    public void sendBlackMsgToAll(Long uid) {
        ONLINE_WS_USER_MAP.forEach((channel, wsChannelExtraDTO) -> {
            wsExecutor.execute(() -> {
                if (wsChannelExtraDTO.getUid().equals(uid)){
                    // 发送消息 您已经被拉黑
                    sendMsg(channel, WebSocketAdapter.buildBlackYourSelfResp());
                }else {
                    // 发送消息 该用户被拉黑
                    sendMsg(channel, WebSocketAdapter.buildBlackOtherResp());
                }
            });
        });
    }


    @Override
    public void connectChannel(Channel channel) {
        // 先保存空对象，因为连接不代表登录，只是建立连接
        ONLINE_WS_USER_MAP.put(channel, new WsChannelExtraDTO());
    }


    /**
     * 登录 请求二维码信息
     * @param channel
     */
    @Override
    public void handleLoginTicket(Channel channel) throws WxErrorException {
        // 生成随机码
        Integer code = generateRandomCode(channel);

        // 找微信申请拿到登录二维码,临时二维码有效期1小时
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) CODE_EXPIRE_TIME.getSeconds());

        // 把二维码推送给前端 适配器进行转换 消息返回类型
//        String ticket = wxMpQrCodeTicket.getTicket();
//        String url = wxMpQrCodeTicket.getUrl();
//        WSBaseResp<WSLoginUrl> wsLoginUrlWSBaseResp = new WSBaseResp<>();
//        wsLoginUrlWSBaseResp.setData(new WSLoginUrl(url));
//        wsLoginUrlWSBaseResp.setType(WSReqTypeEnum.LOGIN.getType());
        WSBaseResp<WSLoginUrl> wsBaseResp = WebSocketAdapter.buildLoginUrlResp(wxMpQrCodeTicket);
        sendMsg(channel, wsBaseResp);
    }

    @Override
    public void disconnectChannel(Channel channel) {
        ONLINE_WS_USER_MAP.remove(channel);
        // todo 这里还可以进行用户下线的广播消息！
    }

    // 发送消息
    private void sendMsg(Channel channel, WSBaseResp<?> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }

    private Integer generateRandomCode(Channel channel) {
        // 生成随机码 如果随机码已经存在 则putIfAbsent 返回非空对象！不存在则返回null 就跳出do while循环
        // 防止重复生成随机码
        Integer code = null;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        }while (ObjectUtil.isNotNull(WAIT_CODE_CHANNEL_MAP.asMap().putIfAbsent(code,channel)));
        return code;
    }


}
